File: Contents\DataContentTests{T}.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.AI.Abstractions.Tests\Microsoft.Extensions.AI.Abstractions.Tests.csproj (Microsoft.Extensions.AI.Abstractions.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Reflection;
using System.Text.Json;
using Xunit;
 
namespace Microsoft.Extensions.AI;
 
public abstract class DataContentTests<T>
    where T : DataContent
{
    private static T Create(params object?[] args)
    {
        try
        {
            return (T)Activator.CreateInstance(typeof(T), args)!;
        }
        catch (TargetInvocationException e)
        {
            throw e.InnerException!;
        }
    }
 
    public T CreateDataContent(Uri uri, string? mediaType = null) => Create(uri, mediaType)!;
 
#pragma warning disable S3997 // String URI overloads should call "System.Uri" overloads
    public T CreateDataContent(string uriString, string? mediaType = null) => Create(uriString, mediaType)!;
#pragma warning restore S3997
 
    public T CreateDataContent(ReadOnlyMemory<byte> data, string? mediaType = null) => Create(data, mediaType)!;
 
    [Theory]
 
    // Invalid URI
    [InlineData("", typeof(ArgumentException))]
    [InlineData("invalid", typeof(UriFormatException))]
 
    // Format errors
    [InlineData("data", typeof(UriFormatException))] // data missing colon
    [InlineData("data:", typeof(UriFormatException))] // data missing comma
    [InlineData("data:something,", typeof(UriFormatException))] // mime type without subtype
    [InlineData("data:something;else,data", typeof(UriFormatException))] // mime type without subtype
    [InlineData("data:type/subtype;;parameter=value;else,", typeof(UriFormatException))] // parameter without value
    [InlineData("data:type/subtype;parameter=va=lue;else,", typeof(UriFormatException))] // parameter with multiple = 
    [InlineData("data:type/subtype;=value;else,", typeof(UriFormatException))] // empty parameter name
    [InlineData("data:image/j/peg;base64,/9j/4AAQSkZJRgABAgAAZABkAAD", typeof(UriFormatException))] // multiple slashes in media type
 
    // Base64 Validation Errors
    [InlineData("data:text;base64,something!", typeof(UriFormatException))]  // Invalid base64 due to invalid character '!'
    [InlineData("data:text/plain;base64,U29tZQ==\t", typeof(UriFormatException))] // Invalid base64 due to tab character
    [InlineData("data:text/plain;base64,U29tZQ==\r", typeof(UriFormatException))] // Invalid base64 due to carriage return character
    [InlineData("data:text/plain;base64,U29tZQ==\n", typeof(UriFormatException))] // Invalid base64 due to line feed character
    [InlineData("data:text/plain;base64,U29t\r\nZQ==", typeof(UriFormatException))] // Invalid base64 due to carriage return and line feed characters
    [InlineData("data:text/plain;base64,U29", typeof(UriFormatException))] // Invalid base64 due to missing padding
    [InlineData("data:text/plain;base64,U29tZQ", typeof(UriFormatException))] // Invalid base64 due to missing padding
    [InlineData("data:text/plain;base64,U29tZQ=", typeof(UriFormatException))] // Invalid base64 due to missing padding
    public void Ctor_InvalidUri_Throws(string path, Type exception)
    {
        Assert.Throws(exception, () => CreateDataContent(path));
    }
 
    [Theory]
    [InlineData("type")]
    [InlineData("type//subtype")]
    [InlineData("type/subtype/")]
    [InlineData("type/subtype;key=")]
    [InlineData("type/subtype;=value")]
    [InlineData("type/subtype;key=value;another=")]
    public void Ctor_InvalidMediaType_Throws(string type)
    {
        Assert.Throws<ArgumentException>("mediaType", () => CreateDataContent("http://localhost/test", type));
    }
 
    [Theory]
    [InlineData("type/subtype")]
    [InlineData("type/subtype;key=value")]
    [InlineData("type/subtype;key=value;another=value")]
    [InlineData("type/subtype;key=value;another=value;yet_another=value")]
    public void Ctor_ValidMediaType_Roundtrips(string mediaType)
    {
        T content = CreateDataContent("http://localhost/test", mediaType);
        Assert.Equal(mediaType, content.MediaType);
 
        content = CreateDataContent("data:,", mediaType);
        Assert.Equal(mediaType, content.MediaType);
 
        content = CreateDataContent("data:text/plain,", mediaType);
        Assert.Equal(mediaType, content.MediaType);
 
        content = CreateDataContent(new Uri("data:text/plain,"), mediaType);
        Assert.Equal(mediaType, content.MediaType);
 
        content = CreateDataContent(new byte[] { 0, 1, 2 }, mediaType);
        Assert.Equal(mediaType, content.MediaType);
 
        content = CreateDataContent(content.Uri);
        Assert.Equal(mediaType, content.MediaType);
    }
 
    [Fact]
    public void Ctor_NoMediaType_Roundtrips()
    {
        T content;
 
        foreach (string url in new[] { "http://localhost/test", "about:something", "file://c:\\path" })
        {
            content = CreateDataContent(url);
            Assert.Equal(url, content.Uri);
            Assert.Null(content.MediaType);
            Assert.Null(content.Data);
        }
 
        content = CreateDataContent("data:,something");
        Assert.Equal("data:,something", content.Uri);
        Assert.Null(content.MediaType);
        Assert.Equal("something"u8.ToArray(), content.Data!.Value.ToArray());
 
        content = CreateDataContent("data:,Hello+%3C%3E");
        Assert.Equal("data:,Hello+%3C%3E", content.Uri);
        Assert.Null(content.MediaType);
        Assert.Equal("Hello <>"u8.ToArray(), content.Data!.Value.ToArray());
    }
 
    [Fact]
    public void Serialize_MatchesExpectedJson()
    {
        Assert.Equal(
            """{"uri":"data:,"}"""{"uri":"data:,"}""",
            JsonSerializer.Serialize(CreateDataContent("data:,"), TestJsonSerializerContext.Default.Options));
 
        Assert.Equal(
            """{"uri":"http://localhost/"}"""{"uri":"http://localhost/"}""",
            JsonSerializer.Serialize(CreateDataContent(new Uri("http://localhost/")), TestJsonSerializerContext.Default.Options));
 
        Assert.Equal(
            """{"uri":"data:application/octet-stream;base64,AQIDBA==","mediaType":"application/octet-stream"}"""{"uri":"data:application/octet-stream;base64,AQIDBA==","mediaType":"application/octet-stream"}""",
            JsonSerializer.Serialize(CreateDataContent(
                uriString: "data:application/octet-stream;base64,AQIDBA=="), TestJsonSerializerContext.Default.Options));
 
        Assert.Equal(
            """{"uri":"data:application/octet-stream;base64,AQIDBA==","mediaType":"application/octet-stream"}"""{"uri":"data:application/octet-stream;base64,AQIDBA==","mediaType":"application/octet-stream"}""",
            JsonSerializer.Serialize(CreateDataContent(
                new ReadOnlyMemory<byte>([0x01, 0x02, 0x03, 0x04]), "application/octet-stream"),
                TestJsonSerializerContext.Default.Options));
    }
 
    [Theory]
    [InlineData("{}")]
    [InlineData("""{ "mediaType":"text/plain" }"""{ "mediaType":"text/plain" }""")]
    public void Deserialize_MissingUriString_Throws(string json)
    {
        Assert.Throws<ArgumentNullException>("uri", () => JsonSerializer.Deserialize<DataContent>(json, TestJsonSerializerContext.Default.Options)!);
    }
 
    [Fact]
    public void Deserialize_MatchesExpectedData()
    {
        // Data + MimeType only
        var content = JsonSerializer.Deserialize<DataContent>("""{"mediaType":"application/octet-stream","uri":"data:;base64,AQIDBA=="}"""{"mediaType":"application/octet-stream","uri":"data:;base64,AQIDBA=="}""", TestJsonSerializerContext.Default.Options)!;
 
        Assert.Equal("data:application/octet-stream;base64,AQIDBA==", content.Uri);
        Assert.NotNull(content.Data);
        Assert.Equal([0x01, 0x02, 0x03, 0x04], content.Data!.Value.ToArray());
        Assert.Equal("application/octet-stream", content.MediaType);
        Assert.True(content.ContainsData);
 
        // Uri referenced content-only 
        content = JsonSerializer.Deserialize<DataContent>("""{"mediaType":"application/octet-stream","uri":"http://localhost/"}"""{"mediaType":"application/octet-stream","uri":"http://localhost/"}""", TestJsonSerializerContext.Default.Options)!;
 
        Assert.Null(content.Data);
        Assert.Equal("http://localhost/", content.Uri);
        Assert.Equal("application/octet-stream", content.MediaType);
        Assert.False(content.ContainsData);
 
        // Using extra metadata
        content = JsonSerializer.Deserialize<DataContent>("""
            {
                "uri": "data:;base64,AQIDBA==",
                "modelId": "gpt-4",
                "additionalProperties":
                {
                    "key": "value"
                },
                "mediaType": "text/plain"
            }
        """{
                "uri": "data:;base64,AQIDBA==",
                "modelId": "gpt-4",
                "additionalProperties":
                {
                    "key": "value"
                },
                "mediaType": "text/plain"
            }
        """, TestJsonSerializerContext.Default.Options)!;
 
        Assert.Equal("data:text/plain;base64,AQIDBA==", content.Uri);
        Assert.NotNull(content.Data);
        Assert.Equal([0x01, 0x02, 0x03, 0x04], content.Data!.Value.ToArray());
        Assert.Equal("text/plain", content.MediaType);
        Assert.True(content.ContainsData);
        Assert.Equal("value", content.AdditionalProperties!["key"]!.ToString());
    }
 
    [Theory]
    [InlineData(
        """{"uri": "data:;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType": "text/plain"}"""{"uri": "data:;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType": "text/plain"}""",
        """{"uri":"data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType":"text/plain"}"""{"uri":"data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType":"text/plain"}""")]
    [InlineData(
        """{"uri": "data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType": "text/plain"}"""{"uri": "data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType": "text/plain"}""",
        """{"uri":"data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType":"text/plain"}"""{"uri":"data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType":"text/plain"}""")]
    [InlineData( // Does not support non-readable content
        """{"uri": "data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", "unexpected": true}"""{"uri": "data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", "unexpected": true}""",
        """{"uri":"data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType":"text/plain"}"""{"uri":"data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType":"text/plain"}""")]
    [InlineData( // Uri comes before mimetype
        """{"mediaType": "text/plain", "uri": "http://localhost/" }"""{"mediaType": "text/plain", "uri": "http://localhost/" }""",
        """{"uri":"http://localhost/","mediaType":"text/plain"}"""{"uri":"http://localhost/","mediaType":"text/plain"}""")]
    public void Serialize_Deserialize_Roundtrips(string serialized, string expectedToString)
    {
        var content = JsonSerializer.Deserialize<DataContent>(serialized, TestJsonSerializerContext.Default.Options)!;
        var reSerialization = JsonSerializer.Serialize(content, TestJsonSerializerContext.Default.Options);
        Assert.Equal(expectedToString, reSerialization);
    }
 
    [Theory]
    [InlineData("application/json")]
    [InlineData("application/octet-stream")]
    [InlineData("application/pdf")]
    [InlineData("application/xml")]
    [InlineData("audio/mpeg")]
    [InlineData("audio/ogg")]
    [InlineData("audio/wav")]
    [InlineData("image/apng")]
    [InlineData("image/avif")]
    [InlineData("image/bmp")]
    [InlineData("image/gif")]
    [InlineData("image/jpeg")]
    [InlineData("image/png")]
    [InlineData("image/svg+xml")]
    [InlineData("image/tiff")]
    [InlineData("image/webp")]
    [InlineData("text/css")]
    [InlineData("text/csv")]
    [InlineData("text/html")]
    [InlineData("text/javascript")]
    [InlineData("text/plain")]
    [InlineData("text/plain;charset=UTF-8")]
    [InlineData("text/xml")]
    [InlineData("custom/mediatypethatdoesntexists")]
    public void MediaType_Roundtrips(string mediaType)
    {
        DataContent c = new("data:,", mediaType);
        Assert.Equal(mediaType, c.MediaType);
    }
}